feat(io): add optional read cache for disk IO - #2567
Conversation
|
/label status/waiting-for-review |
|
Automated pull request review completed. Review effort: Submitted 3 inline comments. |
Merge Protections🟢 All 3 merge protections satisfied — ready to merge. Show 3 satisfied protections🟢 Require kind label
🟢 Require version label
🟢 Require linked issue for feature/bug PRs
|
a59dcda to
44c70ef
Compare
LHT129
left a comment
There was a problem hiding this comment.
Code Review Summary
Thanks for this PR! The page-based read cache is a well-structured addition. Here are my findings:
Already Noted (by self-review)
- Page constructor null check —
Allocatereturn value not checked (page.h) - GetOrLoadPage mutex contention — cache mutex held during disk I/O (basic_io.h)
- PageCache::Insert eviction logic — fallback to
pages_.begin()->first()(page_cache.cpp)
Additional Findings
[suggestion] GetMemoryUsage doesn't account for cache memory
The BasicIO::GetMemoryUsage() method only returns size_, not including the page cache memory. When the read cache is enabled, the actual memory footprint can be significantly larger, leading to inaccurate memory budgeting. Consider adding cache_->Size() * Page::DEFAULT_PAGE_SIZE to the returned value.
[suggestion] Serialize populates cache with transient reads
Serialize calls this->Read(...) which goes through ReadCached when the cache is enabled. This populates the cache with every page read during serialization, potentially evicting useful pages that were already cached. Consider clearing the cache before serialization or adding a bypass flag.
[note] Shrink return removal
The Shrink method previously had return cast().ShrinkImpl(size); which prevented the else branch from executing. Now the return is removed so ClearCache() always executes. This is correct behavior, but worth confirming no ShrinkImpl implementation relied on the old early-return.
[suggestion] DirectReadImpl always sets need_release=true with cache
When the cache is active, the DirectReadImpl overload that returns const uint8_t* always allocates and copies, setting need_release = true. This changes semantics for callers that previously relied on zero-copy reads. Worth documenting.
[note] BucketDataCell GetMemoryUsage under-reports
AdjustBucketReadCacheParam creates separate IO instances per bucket, each with its own cache, but BucketDataCell::GetMemoryUsage() doesn't include cache memory. Consistent with existing IOArray behavior, but worth noting for capacity planning.
Overall Assessment
The design is clean: pluggable eviction policy (PageCache base + LRUPageCache), shared_ptr ownership so evicted pages remain valid for in-flight readers, and proper cache invalidation on writes. The test coverage is good with unit tests for PageCache/LRUPageCache and integration tests for the full read/write/invalidation flow. The examples are helpful for demonstrating usage.
No blocking issues found. The suggestions above are non-blocking improvements.
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1407 changed lines across 39 files).
Submitted 2 inline comments.
Reviewed commit 44c70ef.
44c70ef to
a5f9bb5
Compare
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1420 changed lines across 39 files).
Submitted 4 inline comments.
Reviewed commit a5f9bb5.
LHT129
left a comment
There was a problem hiding this comment.
Code review for PR #2567: Add optional read cache for disk IO.
Summary: This PR introduces a page-based read cache (128KB pages) with LRU eviction for disk IO backends. The cache integrates into the CRTP-based BasicIO<IOTmpl> template and is enabled via IO parameters. IVF bucket datacells distribute the cache budget evenly across buckets.
Overall Assessment: The design is clean and well-integrated into the existing IO abstraction. The cache correctly handles write-through invalidation, resize/shrink/deserialize cache clearing, and cross-page boundary reads. The test coverage is reasonable for unit-level testing of the cache components.
General Notes:
-
[note] DirectReadImpl bypass with cache: When the read cache is enabled,
DirectReadImplis bypassed entirely — the cache path allocates a new buffer, copies data into it viaReadCached, and the caller must callReleaseto free it. This changes the semantics from zero-copy (returning an internal pointer) to always-copy when the cache is active. Callers that rely onneed_release == falsefor performance (e.g.,scan_bucket_by_idinbucket_datacell.h) will now always pay the allocation + copy cost. This is expected given the cache architecture, but worth being aware of for hot paths. -
[suggestion] GetMemoryUsage does not include cache: The
GetMemoryUsage()method only returnssize_, which does not account for the memory consumed by the page cache. When the read cache is enabled, the actual memory footprint can be significantly larger, leading to inaccurate memory budgeting. Consider including cache memory:usage += cache_->Size() * Page::DEFAULT_PAGE_SIZE. -
[suggestion] DirectReadImpl overload documentation: The
DirectReadImploverload (returningconst uint8_t*withneed_release) allocates a temporary buffer and copies throughReadCached. When the cache is active, this always setsneed_release = true. It would be helpful to document this behavioral change in the method comment. -
[note] NonContinuousIO now routes through BasicIO: The change from
inner_io_->WriteImpl(...)toinner_io_->Write(...)(and similarlyMultiReadImpl→MultiRead) correctly routes throughBasicIO's public interface which handles cache invalidation and cache reads. This is a necessary change for read cache correctness. -
[note] Shrink now always clears cache: The
Shrinkmethod previously hadreturn cast().ShrinkImpl(size);which prevented the else branch from executing. NowClearCache()always executes afterShrink. This is correct behavior, but worth confirming noShrinkImplrelies on the old early-return behavior.
See inline comments for specific findings.
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1460 changed lines across 40 files).
Submitted 3 inline comments.
Reviewed commit 5b2e73d.
5b2e73d to
f910bf9
Compare
f910bf9 to
03199f7
Compare
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1500 changed lines across 41 files).
Submitted 1 inline comment.
Reviewed commit 03199f7.
03199f7 to
4d926ac
Compare
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1501 changed lines across 41 files).
Submitted 3 inline comments.
Reviewed commit 4d926ac.
Signed-off-by: LHT129 <tianlan.lht@antgroup.com>
4d926ac to
937ad55
Compare
LHT129
left a comment
There was a problem hiding this comment.
[P2] RebuildSupplementIOParam does not halve cache in hybrid mode
In src/datacell/rabitq_split_datacell.h, RebuildSupplementIOParam (line 1002-1015) builds a custom JSON parameter when supplement_io_type_ is non-empty (hybrid IO mode), but never applies the split_cache halving logic. This means in hybrid mode both the one-bit and supplement cells each get the full cache size — 2× the expected memory budget.
The constructor path (line 186) correctly passes split_cache=true for the supplement cell, but RebuildSupplementIOParam (used during InitIO) only appends the suffix path and IO type override. The read_cache_total_size_ from the original io_param flows through unchanged.
Suggested fix: add the same split_cache logic inside the !supplement_io_type_.empty() branch:
if (io_param->enable_read_cache_) {
json[READ_CACHE_TOTAL_CACHE_SIZE_KEY].SetUint64(io_param->read_cache_total_size_ / 2);
}
vsag-bot
left a comment
There was a problem hiding this comment.
Automated inline review completed.
Review effort: high (1501 changed lines across 41 files).
Submitted 3 inline comments.
Reviewed commit 937ad55.
Summary
Add an optional page-based read cache to concrete disk IO implementations.
Closes #2420
Configuration
Keep the concrete IO type and set
enable_read_cacheto true.total_cache_sizesupplies the page-cache budget.Validation
Local test configuration is blocked by the environment Boost mirror HTTP 403; CI runs the full test suite.